//-------------------------------------------------------------------------------------
// <copyright file="Ping.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation. All rights reserved.
//      THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 
//      EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED 
//      WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
// </copyright>
// <author>Greg Beech [UPDATED: M Fourie 2008-02-12]</author>
// <email>gregbee@microsoft.com</email>
// <date>2004-04-29</date>
// <summary>
//      Pings a computer with a buffer of data to ensure it is visible and the data 
//      does not get corrupted.
// </summary>  
//-------------------------------------------------------------------------------------

namespace Microsoft.Sdc.Tasks
{
    #region Using directives

    using System;
    using System.Globalization;
    using System.Net.NetworkInformation;
    using System.Net.Sockets;
    using System.Threading;
    using Microsoft.Build.Framework;
    using Microsoft.Sdc.Tasks;

    #endregion
    
    #region Class Comments
    /// <summary>
    /// Pings a computer with a buffer of data to ensure it is visible and the data does not get
    /// corrupted.
    /// </summary>
    /// <remarks>
    /// <code><![CDATA[<Ping Machine="machine" Count="count" Interval="interval" Timeout="timeout" BufferSize="bufferSize" AllowFragmentation="allowFragmentation" TimeToLive="timeToLive" LogSuccess="logSuccess"/>]]></code>
    /// <para>where:</para>
    /// <para><i>machine (Required)</i></para>
    /// <para>
    /// The name or IP address of the machine to ping.
    /// </para>
    /// <para><i>count</i></para>
    /// <para>
    /// The number of ping requests to send. The default is four.
    /// </para>
    /// <para><i>interval</i></para>
    /// <para>
    /// The interval in milliseconds between ping requests. The default is no interval.
    /// </para>
    /// <para><i>timeout</i></para>
    /// <para>
    /// The maximum time in milliseconds to wait for a response. The default is five seconds.
    /// </para>
    /// <para><i>bufferSize</i></para>
    /// <para>
    /// The size of the data buffer to send in bytes, which must be between 1 and 65,500 bytes inclusive.
    /// Note that unless fragmentation is allowed then large buffers may fail. The default size is 32 bytes.
    /// </para>
    /// <para><i>allowFragmentation</i></para>
    /// <para>
    /// Whether the packet is allowed to be fragmented. The default is false.
    /// </para>
    /// <para><i>timeToLive</i></para>
    /// <para>
    /// The time-to-live for the packet, that is the number of times it can be forwarded by gateways and 
    /// routers before the packet is deemed undeliverable and discarded. The default is 128.
    /// </para>
    /// <para><i>logsuccess</i></para>
    /// <para>
    /// Indicates whether to log successful pings to the console. The default is false.
    /// </para>
    /// <para><i>failureCount (Output)</i></para>
    /// <para>
    /// The number of ping requests that failed.
    /// </para>
    /// <para><i>roundTripTime (Output)</i></para>
    /// <para>
    /// The average round trip time for the ping requests.
    /// </para>
    /// </remarks>
    /// <example>
    /// <code><![CDATA[
    /// <Project>
    ///     <Target Name="Test">
    ///         <Ping 
    ///                Machine="mymachine" 
    ///                Count="2"
    ///                Interval="1000"
    ///                Timeout="3000" 
    ///                BufferSize="1024" 
    ///                AllowFragmentation="false" 
    ///                TimeToLive="128"
    ///                StopOnSuccess="true"
    ///                LogSuccess="true">
    ///         <Output TaskParameter="FailureCount" PropertyName="FailedPingCount" />
    ///         <Output TaskParameter="RoundTripTime" PropertyName="RoundTripDuration" />
    ///         </Ping>
    ///     </Target>
    /// </Project>
    /// ]]></code>    
    /// </example>
    #endregion
    public class Ping : TaskBase
    {
        #region Member Variables

        private bool allowFragmentation;
        private int bufferSize = 32;
        private int count = 4;
        private int interval;
        private string machine;
        private int timeout = 5000;
        private int timeToLive = 128;
        private int failureCount;
        private long roundTripTime;
        private bool stopOnSuccess;
        private bool logSuccess;
        #endregion

        #region Properties
        /// <summary>
        /// If TRUE the pings stop as soon as a good one returns.
        /// </summary>
        /// <value>Defaults to FALSE.</value>
        public bool StopOnSuccess
        {
            get
            {
                return this.stopOnSuccess;
            }

            set
            {
                this.stopOnSuccess = value;
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether to log successful pings to the console
        /// </summary>
        /// <value><c>true</c> if [log success]; otherwise, <c>false</c>.</value>
        public bool LogSuccess
        {
            get
            {
                return this.logSuccess;
            }

            set
            {
                this.logSuccess = value;
            }
        }

        /// <summary>
        /// Gets or sets whether the ping request is allowed to fragment the packet. The default is <b>false</b>.
        /// </summary>
        /// <value>
        /// <b>true</b> if the packet can be fragmented, or <b>false</b> otherwise.
        /// </value>
        public bool AllowFragmentation
        {
            get
            {
                return this.allowFragmentation;
            }

            set
            {
                this.allowFragmentation = value;
            }
        }

        /// <summary>
        /// Gets or sets the size of the data buffer in bytes sent with the ping request. The default is 32.
        /// </summary>
        /// <value>
        /// An integer between 1 and 65,500.
        /// </value>
        public int BufferSize
        {
            get
            {
                return this.bufferSize;
            }

            set
            {
                if (value < 1 || value > 65500)
                {
                    throw new ArgumentOutOfRangeException("value", value, "The buffer size must be between 1 and 65,500 bytes inclusive.");
                }

                this.bufferSize = value;
            }
        }

        /// <summary>
        /// Gets or sets the number of ping requests that are sent. The default is four.
        /// </summary>
        /// <value>
        /// A positive integer.
        /// </value>
        public int Count
        {
            get
            {
                return this.count;
            }

            set
            {
                if (value < 1)
                {
                    throw new ArgumentOutOfRangeException("value", value, "The number of ping requests must be greater than zero.");
                }

                this.count = value;
            }
        }

        /// <summary>
        /// Gets the number of pings that failed.
        /// </summary>
        /// <value>
        /// Zero if all pings were successful, or the number of pings that failed otherwise.
        /// </value>
        [Output]
        public int FailureCount
        {
            get
            {
                return this.failureCount;
            }
        }

        /// <summary>
        /// Gets or sets the interval in milliseconds between ping requests.
        /// </summary>
        /// <value>
        /// The interval in milliseconds between ping requests.
        /// </value>
        public int Interval
        {
            get
            {
                return this.interval;
            }

            set
            {
                if (value < 0)
                {
                    throw new ArgumentOutOfRangeException("value", value, "The ping interval must be greater than or equal to zero.");
                }

                this.interval = value;
            }
        }

        /// <summary>
        /// Gets or sets the name or IP address of the machine to ping.
        /// </summary>
        /// <value>
        /// The name or IP address of the machine to ping.
        /// </value>
        [Required]
        public string Machine
        {
            get
            {
                return this.machine;
            }

            set
            {
                this.machine = value;
            }
        }

        /// <summary>
        /// Gets the average round trip time in milliseconds for all successful pings. If no pings were
        /// successful then this returns zero.
        /// </summary>
        /// <value>
        /// The average round trip time in milliseconds.
        /// </value>
        [Output]
        public long RoundTripTime
        {
            get
            {
                return this.roundTripTime;
            }
        }

        /// <summary>
        /// Gets or sets the maximum time in milliseconds to wait for a response.
        /// </summary>
        /// <value>
        /// The maximum time in milliseconds to wait for a response.
        /// </value>
        public int Timeout
        {
            get
            {
                return this.timeout;
            }

            set
            {
                if (value < 1)
                {
                    throw new ArgumentOutOfRangeException("value", value, "The timeout must be at least one millisecond.");
                }

                this.timeout = value;
            }
        }

        /// <summary>
        /// Gets or sets the time-to-live for the packet, that is the number of times it can be forwarded
        /// by gateways and routers before the packet is deemed undeliverable and discarded.
        /// </summary>
        /// <value>
        /// The maximum number of times the packet can be forwarded.
        /// </value>
        public int TimeToLive
        {
            get
            {
                return this.timeToLive;
            }

            set
            {
                if (value < 1)
                {
                    throw new ArgumentOutOfRangeException("value", value, "The time-to-live must be greater than zero.");
                }

                this.timeToLive = value;
            }
        }

        #endregion

        #region Methods
        /// <summary>
        /// Executes the task.
        /// </summary>
        protected override void InternalExecute()
        {
            this.Log.LogMessage(string.Format(CultureInfo.InvariantCulture, "Pinging machine '{0}'.", this.Machine));
            byte[] buffer = this.CreatePingBuffer();
            long totalRoundTripTime = 0L;
            this.failureCount = 0;
            bool success = false;
            using (System.Net.NetworkInformation.Ping pinger = new System.Net.NetworkInformation.Ping())
            {                
                PingOptions options = new PingOptions(this.TimeToLive, !this.AllowFragmentation);
                for (int i = 0; i < this.count; i++)
                {
                    // If we are stopping on success and we have succeeded already then exit
                    if (this.StopOnSuccess && success)
                    {
                        break;
                    }

                    try
                    {
                        PingReply response = pinger.Send(this.Machine, this.Timeout, buffer, options);
                        if (response.Status == IPStatus.Success)
                        {
                            // ping reported success so check the returned buffer is correct
                            if (CompareBytes(buffer, response.Buffer))
                            {
                                totalRoundTripTime += response.RoundtripTime;
                                if (this.LogSuccess)
                                {
                                    this.Log.LogMessage(response.Status + ": " + response.RoundtripTime + "ms");
                                }

                                success = true;
                            }
                            else
                            {
                                this.Log.LogWarning(string.Format(CultureInfo.InvariantCulture, "Ping number {0} for machine '{1}' reported success but the returned buffer was corrupted.", i + 1, this.Machine));
                                this.failureCount++;
                            }
                        }
                        else
                        {
                            // ping reported failure
                            this.Log.LogWarning(string.Format(CultureInfo.InvariantCulture, "Ping number {0} for machine '{1}' failed with status '{2}'.", i + 1, this.Machine, response.Status));
                            this.failureCount++;
                        }
                    }
                    catch (SocketException ex)
                    {
                        this.failureCount++;
                        // DNS failed to resolve the machine name
                        if (!this.StopOnSuccess)
                        {
                            this.Log.LogError(string.Format(CultureInfo.InvariantCulture, "DNS failed to resolve the machine name '{0}': {1}", this.Machine, ex.Message));
                        }
                    }
                    catch (PingException ex)
                    {
                        this.failureCount++;
                        if (!this.StopOnSuccess)
                        {
                            string exception = ex.Message;
                            if (string.IsNullOrEmpty(ex.InnerException.Message) == false)
                            {
                                exception += "(" + ex.InnerException.Message + ")";
                            }

                            this.Log.LogError(string.Format(CultureInfo.InvariantCulture, "Ping exception: {0}", exception));
                        }
                    }
                    catch (Exception ex)
                    {
                        this.failureCount++;
                        if (!this.StopOnSuccess)
                        {
                            string exception = ex.Message;
                            if (string.IsNullOrEmpty(ex.InnerException.Message) == false)
                            {
                                exception += "(" + ex.InnerException.Message + ")";
                            }

                            this.Log.LogError(string.Format(CultureInfo.InvariantCulture, "Unhandled exception: {0}", exception));
                        }
                    }

                    if (this.Interval > 0)
                    {
                        Thread.Sleep(this.Interval);
                    }
                }

                if (this.StopOnSuccess && !success)
                {
                    this.Log.LogError("Ping failed waiting for success");
                    return;
                }
            }

            // set the round trip time if at least one ping succeeded
            if (this.count - this.failureCount > 0)
            {
                this.roundTripTime = totalRoundTripTime / (this.count - this.failureCount);
            }
        }

        /// <summary>
        /// Compares the contents of two byte arrays to see if they are the same.
        /// </summary>
        /// <param name="expected">
        /// The expected array.
        /// </param>
        /// <param name="actual">
        /// The actual array.
        /// </param>
        /// <returns>
        /// <b>true</b> if the contants of the arrays are the same, or <b>false</b> otherwise
        /// </returns>
        private static bool CompareBytes(byte[] expected, byte[] actual)
        {
            if (expected == null)
            {
                throw new ArgumentNullException("expected");
            }

            if (actual == null)
            {
                throw new ArgumentNullException("actual");
            }

            bool same = true;
            if (actual.Length != expected.Length)
            {
                same = false;
            }
            else
            {
                for (int i = 0; i < expected.Length; i++)
                {
                    if (actual[i] != expected[i])
                    {
                        same = false;
                    }
                }
            }

            return same;
        }

        /// <summary>
        /// Creates a buffer of data to send with the ping request.
        /// </summary>
        /// <returns>
        /// An array of bytes.
        /// </returns>
        private byte[] CreatePingBuffer()
        {
            byte[] buffer = new byte[this.bufferSize];
            for (int i = 0; i < buffer.Length; i++)
            {
                buffer[i] = unchecked((byte)i);
            }

            return buffer;
        }
        #endregion
    }
}